[Amazon Connect] IVRで無人の予約受付を作ってみました 〜居酒屋の予約の場合〜
1 はじめに
AIソリューション部の平内(SIN)です。
Amazon Connect(以下、Connect)では、IVRを使用してフローを構築することができます。
今回は、IVRだけで処理し、無人対応する「居酒屋の予約」をサンプルとして作ってみました。
なお、本記事で紹介しているのは、UIの部分だけであり、実施の予約を扱うロジック(バックエンドのAPIなど)については、全てモックで有ることを予めご了承下さい。
最初に、作成したサンプルを使用している様子です。(注:居酒屋クラメソ札幌南口店は実在しません)
2 問い合わせフロー
簡単なIVRであれば、問い合わせフローに、いくつかのブロックを置くだけでも作成することが可能です。
しかし、入力に対するバリデーションや、再帰的な流れをきめ細かく表現していくと、あっという間に問い合わせフローが肥大化し、メンテナンス不能になってしまいます。
今回は、メッセージの生成や、分岐についてを全てLambdaで処理し、これを中心に問い合わせフローを作成しました。
(1) Lambda中心
Lambda中心というのは、次のようなイメージです。
Lambdaファンクションを中心に置き、その他の処理をいくつかにパターン化して配置します。そして特定の処理以外は、また、Lambdaに戻るようになっています。
Lambdaでは、セッションデータを管理し、その状態に応じたメッセージを生成し、後の処理の分岐を行っています。
(2) 問い合わせフロー
実際に、今回、作成した問い合わせフローです。バリデーションや、再帰的なロジックが入っている割にはシンプルになってます。
Lambda以外の処理は、以下の3パターンとなっています。
- メッセージを再生して顧客の入力を保存(1文字)
- メッセージを再生して顧客の入力を保存(4文字)
- メッセージを再生して終了
3 実装
(1) index.ts
Lambdaのメインの処理も比較的シンプルです。 ビジネスロジック(予約の処理)については、全てReserveクラスで処理しています
ConnectRequestでは、Connectからの入力の型情報を定義しています。
import Reserve from './Reserve'; import ConnectRequest from './ConnectRequest'; declare var exports: any; exports.handle = async (event: ConnectRequest) => { console.log(JSON.stringify(event)); const phoneNumber = event.Details.ContactData.CustomerEndpoint.Address; // 顧客の発信番号 const params = event.Details.Parameters; // 入力パラメータ const reserve = new Reserve(params.reserve, phoneNumber); // 予約クラスの生成(前回エクスポートしたデータで初期化する) const message = await reserve.input(params.inputData); // inputDataを処理する return { reserve: reserve.exportData, // データのエクスポート next: reserve.next, // 次のアクション message: message // 再生するメッセージ }; }
(2) Reserve.ts(セッションデータの維持)
Reserveクラスでは、セッション情報を保持するために、終了時に文字列化したデータを返し(exportData())、生成時に、前回のデータで初期化しています(constructor())。
このセッションデータを問い合わせフローの属性で維持する手法は、以前、下記の記事で紹介させて頂いたものと同じです。
参考:[Amazon Connect] 問い合わせフローの中で使用するLambdaのセッション情報を保持するクラスを作ってみた
export default class Reserve { // ・・・略・・・ // コンタクトフローに以前のデータが有る場合は、内部データを復元する constructor(data: string, phoneNumber: string) { this._phoneNumber = phoneNumber; if(data) { const tmp = JSON.parse(data); this._mode = tmp.mode; this._numberOfPeople = tmp.numberOfPeople; this._month = tmp.month; this._day = tmp.day; this._hour = tmp.hour; this._min = tmp.min; } } // コンタクトフローに保存するために、内部データをテキスト化する get exportData() { const tmp = { mode: this._mode, numberOfPeople: this._numberOfPeople, month: this._month, day: this._day, hour: this._hour, min: this._min, } return JSON.stringify(tmp); } // ・・・略・・・
(3) Reserve.ts(入力データの処理)
Reserveクラスでは、予約日付、時間、人数がプライベートな変数として定義されており、ユーザーの入力値で、逐次設定されます。(input(inputData: string))
input(inputData: string)では、入力値のバリデーションなどを行い、有効であれば、変数に保管し、無効であればエラーメッセージを返すなどの処理が記述されています。
export default class Reserve { private _mode: number = 0; // 0:Welcome 1:Date 2:Time 3:Customers 4:Confirm private _numberOfPeople: number = 0; private _month: number = 0; private _day: number = 0; private _hour: number = 0; private _min: number = 0; private _phoneNumber: string = ''; next: Next; // 入力を処理して、メッセージを生成する async input(inputData: string): Promise { let message = ''; let errorMessage = undefined; switch(this._mode) { case 0: //Welcome message += this._welcomeMessage(); message += this._dateMessage(); this._mode++; break; case 1: // SetDate errorMessage = this.setDate(inputData); if(errorMessage) { // 入力エラーなので、もう一度 message += errorMessage + this._dateMessage(); } else { message += this._timeMessage(); this._mode++; } break; case 2 : // SetTime errorMessage = this.setTime(inputData); if(errorMessage) { // 入力エラーなので、もう一度 message += errorMessage + this._timeMessage(); } else { message += this._numberOfPeopleMessage(); this._mode++; } break; case 3 : // SetNumberOfPeople errorMessage = this.setNumberOfPeople(inputData); if(errorMessage) { // 入力エラーなので、もう一度 message += errorMessage + this._numberOfPeopleMessage(); } else { message += this._confirmMessage(); this._mode++; } break; case 4 : // Confirm if(inputData != '1') { // 最初から message += this._againMessage() + this._dateMessage(); this._mode = 1; //SetDate } else { // 終了 // ショートメール送信 const sns = new SNS(); let body = '[予約メール確認]\n'; body += '下記の内容でご予約を承りました。\n'; body += '日時:' + this._dateStr() + ' \n'; body += this._numberOfPeople + '名様\n'; body += '居酒屋クラメソ\n'; body += '札幌南口店 001-xxx-xxxx\n'; await sns.send(this._phoneNumber, body); message += this._goodbyMessage(); } break; } message += ''; return message; } //・・・略・・・
(4) Reserve.ts(バリデーション)
入力値のバリデーションの一例です。
入力値の文字数は適切か?、時間として正しいか?また、仕様上問題ないか(営業時間内か?)などが点検されています。
setTime(inputData: string): string|undefined { let errorMessage = '時間の入力が無効です。もう一度、お伺いします '; try{ if(inputData.length == 4) { const hour = Number(inputData.slice(0,2)); const min = Number(inputData.slice(2,4)); if( 0 <= min && min < 60) { if(18 <= hour && hour <= 23) { if(min == 0 || min == 30){ // 正常値なので保存 this._hour = hour; this._min = min; return undefined; } return '予約可能な時間は30分単位です。もう一度、お伺いします ' } else { return '予約が可能な時間は18時から23時までです。もう一度、お伺いします ' } } } } catch (error) { } return errorMessage; }
全てのコードは下記におきました。
[GitHub] https://github.com/furuya02/izakaya-reservation
4 最後に
今回は、IVRだけで処理するフローを作成してみました。
肥大化したり、複雑になった問い合わせフローは、非常にメンテナンスが辛いです。 問い合わせフローについては、可能な限りシンプルなものとし、ほとんど全てのロジックをLambdaで実装する今回の手法は、比較的複雑なIVRを作る場合のベストプラクティスではないかと、個人的には感じています。
弊社では、「Amazon Connect」の導入を検討している方を対象とした無料相談会を毎週開催中です。
また、音声を利用した各種ソリューションの導入支援を行っております。お気軽にお問い合わせください。